package sweet.helper

import java.io._
import scala.io.Source

/** Companion object to FileHelper. This object contain a implicit
 * converter for java.io.File to FileHelper objects. */
object FileHelper{
  val LINE_SEP = System.getProperty("line.separator")
  val DEFAULT_BLOCK_SIZE = 1024*8
  
  /** Import this method to your namespace if you want use any FileHelper methods
   * directly from your java.io.File instance! */
  implicit def file2helper(file: File) = new FileHelper(file)  
  
  /** Create a empty file to the give path, then return the FileHelper object for it. */
  def touch(path: String): FileHelper = {
    val f = new File(path)
    if(!f.getParentFile.exists) f.getParentFile.mkdirs
    f.writeText("")
    new FileHelper(f)
  }
  /** Create all the actual path(s) to the given directory path name, then return the FileHelper object for it. */
  def mkdirs(path: String): FileHelper = { 
    val f = new File(path)
    f.mkdirs
    new FileHelper(f)
  }
}

import FileHelper.DEFAULT_BLOCK_SIZE

/** Java File extension. Provide many more convinient methods to allow working
 * with files more practically and fast.
 */
class FileHelper(val file: File, blockSize: Int) {
  def this(name: String) = this(new File(name), DEFAULT_BLOCK_SIZE)
  def this(file: File) = this(file, DEFAULT_BLOCK_SIZE)
  def this(parent: File, filename: String) = this(new File(parent, filename), DEFAULT_BLOCK_SIZE)
  
  /** We can compare either a FileHelper or File object */
  override def equals(that: Any):Boolean = {
    if(that.isInstanceOf[FileHelper])
      file == that.asInstanceOf[FileHelper].file
    else if(that.isInstanceOf[File])
      file == that.asInstanceOf[File]
    else
      throw new Exception("We can only compare to FileHelper or File object.")
  }
  /** Use underlying file.hashCode. */
  override def hashCode = file.hashCode
  
  /** Split "basename" and "extention" from a filename. The separator is NOT included! */
  def parseFileExt(sep: String): (String, String) = { 
    val name = file.getName
    val idx = name.lastIndexOf(sep)
    (name.substring(0, idx), name.substring(idx+1))
  }
  
  /** Return just the basename of filename. */
  def getBasename = parseFileExt(".")._1
  /** Return just the extension of filename. */
  def getExt = parseFileExt(".")._2
  
  /** If file is not abolute, then get only the path upto where it first defined. */
  def getPathname: String = {
    if(file.isAbsolute) file.getAbsolutePath
    else{
      var f = file //init file path name
      val sb = new StringBuilder(f.getName)
      while(f.getParentFile != null){
        f = f.getParentFile
        sb.insert(0, f.getName+File.separator)
      }
      sb.toString
    }
  }
  
  /** List file in descending order. If there is not file under this dir, return empty List, not null! */
  def listSortedFiles: List[File] = listSortedFiles((f1,f2)=>f1.compareTo(f2)<0)
  
  /** List file according to your own comparator. 
   * If there is not file under this dir, return empty List, not null! */
  def listSortedFiles(cmp: (File,File)=>Boolean): List[File] = { 
    val files = file.listFiles
    if(files == null) Nil
    else files.toList.sort(cmp)
  }
  
  /** Walk this file as dir recursively, calling a user func for each file AND dir it
   * visits. We will pass a File object for the file or dir, and a a string path to each
   * user func. The string path is in relative to dir where walk started, wich is 
   * an empty string first. A directory is called with user function alter all files are visited first.*/
  def walk(func: (File, String)=>Unit){
    def walkWithPath(f: File, path: String){
      for(subfile <- new FileHelper(f).listSortedFiles){
        if(subfile.isDirectory){
          if(path=="")
            walkWithPath(subfile, subfile.getName)
          else
            walkWithPath(subfile, path+"/"+subfile.getName)
          func(subfile, path) //call user func as dir.
        }else 
          func(subfile, path) //call user func as file.
      }
    }
    walkWithPath(file, "")
  }
  
  /** Delete all files recursively for this dir, including the directory itself.*/
  def deleteAll{
    if(file.isDirectory)
      walk{ (f,path) => f.delete }
    file.delete //delete this
  }
  
  /** For each sub file in a given dir, call user function.*/
  def eachFile(func: File=>Unit){
    walk{ (f, path) =>  if(f.isFile) func(f) }
  }
  
  /** For each subdir call user function.*/
  def eachDir(func: File=>Unit){    
    walk{ (f,path) =>  if(f.isDirectory) func(f) }
  }
  
  /** Copy this file dir recusively to dest. 
   * If from/this file object is a dir, then dest MUST be a dir or else exception will be thrown.
   * If from/this file object is a file, then dest can be either dir(copy into) or file (overwrite). */
  def copyTo(dest:File): Unit ={
    /** Copy this file into a dest. If dest is a directory, then copy into it. else overwrite dest file. */
    def copyFile(file: File, dest: File){
      if(dest.isDirectory){
        val destFile = new File(dest, file.getName)
        StreamHelper.copyStream(new FileInputStream(file), new FileOutputStream(destFile), blockSize)
      }else{
        StreamHelper.copyStream(new FileInputStream(file), new FileOutputStream(dest), blockSize)    
      }
    }
    if(file.isFile) 
      copyFile(file, dest)
    else{
      if(!dest.isDirectory) throw new Exception("Can't copy this directory to a file!")
      walk{ (f, path) =>
        val subDest = new File(dest, path)
        copyFile(f, subDest)
      }
    }
  }
  
  /** Give user callback function on a BufferedReader to this file. It will auto close resource after use.*/
  def withReader(func: BufferedReader=>Unit){
    val reader = new BufferedReader(new FileReader(file));
    try{ func(reader) }
    finally{ reader.close }
  }  
  /** Give user callback function on a BufferedWriter to this file. It will auto close resource after use.*/
  def withWriter(func: BufferedWriter=>Unit){
    val writer = new BufferedWriter(new FileWriter(file));
    try{ func(writer) }
    finally{ writer.close }
  }  
  /** Give user callback function on a PrintWriter to this file. It will auto close resource after use.*/
  def withPrintWriter(func: PrintWriter=>Unit){
    withWriter{ writer =>
      val out = new PrintWriter(writer)
      func(out)
    }
  }
  /** Give user callback function on a OutputStream to this file. It will auto close resource after use.*/
  def withOutputStream(func: OutputStream=>Unit){
    val ostream = new FileOutputStream(file);
    try{ func(ostream) }
    finally{ ostream.close }
  }  
  /** Give user callback function on a OutputStream to this file. It will auto close resource after use.*/
  def withInputStream(func: InputStream=>Unit){
    val istream = new FileInputStream(file);
    try{ func(istream) }
    finally{ istream.close }
  }  
  /** Write a single block of string to this file. If file exists, it will get truncated. */  
  def writeText(text: String)={
    withPrintWriter{ out => out.print(text) }
  }  
  /** Write lines of string into this file. If file exists, it will get truncated. */
  def writeLines(lines: Seq[String]){
    withPrintWriter{ out => for(ln <- lines) out.println(ln) }
  }
  /** Read all text from this file into memory and return as String object */
  def readText: String = {
    val sb = new StringBuilder()
    eachBlock(blockSize){ buf => sb.append(new String(buf)) }
    sb.toString
  }
  /** Read all text from this file into memory and return all lines as List object */
  def readLines: List[String] = {
    val lines = new scala.collection.mutable.ListBuffer[String]
    var line: String = null
    withReader{ reader =>
      while({ line = reader.readLine; line != null })
        lines.append(line)
    }
    lines.toList
  }
  /** Process each line in this text file and call user function to process it. This 
   * will not load entire file into memory, so very efficient in process large file! */
  def eachLine(process: (String)=>Unit){
		StreamHelper.eachLine(new FileInputStream(file)){ ln => process(ln) }
  }
  
  /** Same as #eachLien, but pass the line number(1 based index) for each line it process 
   * to user function as well. */
  def eachLineWithNumber(func: (String, Int)=>Unit){
    var i = 0
    eachLine{ ln => i += 1; func(ln, i) }    
  }
  
  /** Call user function for each byte of this file. For more effiecient processing, try #eachBlock(int)
   * that let you specify block size! */
  def eachByte(process: (Byte)=>Unit){
    eachBlock(blockSize){ buf => for(b <- buf) process(b) }
  }
  
  /** Process each blocks of bytes in this binary file and call user function to process it. This 
   * will not load entire file into memory, so very efficient in process large file! */
  def eachBlock(size: Int)(process: (Array[Byte])=>Unit){
		StreamHelper.eachBlock(new FileInputStream(file), size){ buf => process(buf) }
  }
}
